﻿using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reactive;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Reflection;

namespace WcfDynamicProxy
{
    /// <summary>
    /// Представляет запрос к сервису. 
    /// </summary>
    internal class Query
    {
        public static IObservable<TResult> Execute<TService, TResult>(IAsyncChannel<TService> asyncChannel,
                                                                      Expression<Func<TService, TResult>> call,
                                                                      TaskManager taskManager,
                                                                      Action beforeReleaseToken)
        {
            return new QueryClosure<TService, TResult>(asyncChannel, call).QueryResult(taskManager, beforeReleaseToken);
        }

        public static IObservable<Unit> Execute<TService>(IAsyncChannel<TService> asyncChannel,
                                                          Expression<Action<TService>> call,
                                                          TaskManager taskManager,
                                                          Action beforeReleaseToken)
        {
            return new QueryClosure<TService, Unit>(asyncChannel, call).QueryResult(taskManager, beforeReleaseToken);
        }

        /// <summary>
        /// Замыкание (внешние переменные) запроса.
        /// </summary>
        /// <typeparam name="TService"></typeparam>
        /// <typeparam name="TResult"></typeparam>
        private class QueryClosure<TService, TResult>
        {
            private readonly MethodCallExpression call;
            private readonly IAsyncChannel<TService> asyncChannel;

            public QueryClosure(IAsyncChannel<TService> asyncChannel, Expression<Func<TService, TResult>> call)
                : this(asyncChannel, (MethodCallExpression) call.Body)
            {
            }

            public QueryClosure(IAsyncChannel<TService> asyncChannel, Expression<Action<TService>> call)
                : this(asyncChannel, (MethodCallExpression) call.Body)
            {
            }

            private QueryClosure(IAsyncChannel<TService> asyncChannel, MethodCallExpression call)
            {
                this.call = call;

                this.asyncChannel = asyncChannel;
            }

            public IObservable<TResult> QueryResult(TaskManager taskManager, Action beforeReleaseThread)
            {
                var token = taskManager.GetExecutionToken();

                var res = token.SelectMany(
                    t =>
                        {
                            var asyncSubject = new AsyncSubject<TResult>();
                            GetBeginMethod().Invoke(asyncChannel.Channel,
                                                    BuildParameters( // FromAsync не сработал, поэтому реализую в ручную.
                                                        ar =>
                                                            {
                                                                try
                                                                {
                                                                    TraceLog("Invoke Callback " + call.Method.Name);

                                                                    TResult result = default(TResult);                                                                    
                                                                    if (typeof(TResult) == typeof(Unit))
                                                                    {
                                                                        GetEndMethod().Invoke(asyncChannel.Channel,
                                                                                                  new object[] { ar });
                                                                    }
                                                                    else
                                                                    {
                                                                        result =
                                                                            (TResult)
                                                                            GetEndMethod().Invoke(asyncChannel.Channel,
                                                                                                  new object[] {ar});
                                                                    }                                                                

                                                                    TraceLog("EndMethod " + call.Method.Name);
                                                                        asyncSubject.OnNext(result);
                                                                        asyncSubject.OnCompleted();
                                                                }
                                                                catch (Exception ex)
                                                                {
                                                                    var targetInvocationException = ex as TargetInvocationException;
                                                                    var innerException =
                                                                        targetInvocationException == null
                                                                                             ? ex
                                                                                             : targetInvocationException
                                                                                                   .InnerException;

                                                                    TraceLog("Error " + call.Method.Name);
                                                                    asyncSubject.OnError(innerException);
                                                                }
                                                            }));

                            return asyncSubject.Finally(() =>
                                                            {
                                                                beforeReleaseThread();
                                                                t.Dispose();
                                                            });
                        }
                    );

                var publishedResult = res.PublishLast();

                publishedResult.Connect();

                return publishedResult.Finally(() => { TraceLog("Query finished"); });
            }


            private MethodInfo GetBeginMethod()
            {
                return GetMethod("Begin" + call.Method.Name);
            }

            private MethodInfo GetEndMethod()
            {
                return GetMethod("End" + call.Method.Name);
            }

            private MethodInfo GetMethod(string methodName)
            {
                return asyncChannel.Channel.GetType().GetMethod(methodName);
            }

            private object[] BuildParameters(Action<IAsyncResult> onCallCompleted)
            {
                var parameters = new List<object>();

                foreach (Expression argument in call.Arguments)
                {
                    object parameter = Expression.Lambda(argument).Compile().DynamicInvoke();
                    parameters.Add(parameter);
                }

                parameters.Add(new AsyncCallback(onCallCompleted));
                parameters.Add(null);

                return parameters.ToArray();
            }

            private void TraceLog(string message)
            {
                // TODO импортировать логер в конструкторе
                //System.Diagnostics.Debug.WriteLine("Query:" + message);
            }
        }
    }
}